第4週 配列・文字列
前回の復習
fizz-buzz
1~50までの自然数に対して3で割り切れるものはFizz, 5で割り切れるものはBuzz, 両方で割り切れるものはFizzBuzz, それ以外であればその数を一行毎に出力する。
まずfor文で1~50までループする構造を書く。(コード例の名前の最初の%はこのコードがまだ完成していないことをあえて名前で表している)
code:%fizzbuzz.c
int main() {
for(int i = 1; i <= 50; i++) {
}
return 0;
}
次にそれぞれのループ内で数iに対して条件分岐を行い、求められる文字列を出力する処理を記述する。
code:%fizzbuzz.c
int main() {
for(int i = 1; i <= 50; i++) {
if(i % 3 == 0) {
puts("Fizz");
} else if(i % 5 == 0) {
puts("Buzz");
} else if(i % 15 == 0) {
puts("FizzBuzz");
} else {
printf("%d\n", i);
}
}
return 0;
}
puts関数はここで初めて登場したが、引数を一つだけとり、その文字列を表示した後改行する。つまりputs(str);はprintf("%s\n", str);と等価である。
このコードはコンパイルは通るが正常に動作しない。具体的には、最初および2番目の条件分岐で3番目にマッチするものが処理されてしまう。したがって、条件の厳しい3番目の分岐から記述してやる必要がある。
code:fizzbuzz.c
int main() {
for(int i = 1; i <= 50; i++) {
if(i % 15 == 0) {
puts("FizzBuzz");
} else if(i % 3 == 0) {
puts("Fizz");
} else if(i % 5 == 0) {
puts("Buzz");
} else {
printf("%d\n", i);
}
}
return 0;
}
forやifでは内部の文が一つだけであるならば{}を省略できるため、for文内は次のように短くできる。
code:short_fizzbuzz.c
for(int i = 1; i <= 50; i++) {
if(i % 15 == 0) puts("FizzBuzz");
else if(i % 3 == 0) puts("Fizz");
else if(i % 5 == 0) puts("Buzz");
else printf("%d\n", i);
}
余談
プログラミング言語の仕様を熟知したプログラマは可読性を無視しプログラムを可能な限り短く記述するコードゴルフと呼ばれる競技を行うことがある。FizzBuzzはその対象として最もポピュラーなプログラムでもある。
code:shortest_fizzbuzz.c
main(i){for(;i<51;puts(i++%5?"":"Buzz"))printf(i%3?i%5?"%d":0:"Fizz",i);}
コードの動作についてはここでは説明しないので各自で調べてみてほしい。
even-odd
標準入力から自然数値を受け取り、その値の偶奇でevenまたはoddを出力する。
scanf関数を用いて標準入力からint型の変数に値を代入することができればあとはfizzbuzzより簡単な問題である。
偶奇を調べるには実際に2で割った余りを求め、条件分岐すれば良い。
code:evenodd.c
int main() {
int input;
scanf("%d", &input);
if(input % 2 == 0) {
puts("even");
} else {
puts("odd");
}
return 0;
}
これでも解答たり得るが、C言語的には多少の可読性と引き換えにコードを簡潔にできる。
code:short_evenodd.c
int main() {
int input;
scanf("%d", &input);
if(input % 2) puts("odd");
else puts("even");
return 0;
}
input % 2は0または1であるから、これ自体が真偽値となり得るため、わざわざ== 0を付け足す必要性はあまりなく、直接oddを出力すればより簡潔なコードを書くことができる。
これはC言語の真偽値が0または1であることに根ざした記法であるため、C言語をよく書くプログラマには通じるがそうでないプログラマにとっては直感的な記法ではない。したがって状況に合わせて== 0のような冗長だが可読性を上げることのできるコードを付け足すかどうか判断してほしい。
配列とは
配列とは同じ型の値である要素の集まりを一列にまとめたものである。
配列を用いることで関連する値を管理しやすくなる。
関連するような値に1つ1つ変数を用意すると効率が悪い時に配列を使う。
どんな時に配列を使うと良いか
・並び順管理が必要なデータ処理
・エクセルのような複数行のデータ処理
・座席の空席管理
・CSVデータの処理(二次元配列、連想配列)
・将棋のような盤面の表現(二次元配列)
1次元配列
配列の宣言
配列は次のように型の種類と変数名、[]の中に要素数を与えて宣言する。
code:array0.c
int main(){
return 0;
}
int型の要素数5のaという名前の配列を宣言した。aは実際にはint*型であり、int型とは異なることに注意する必要がある。
配列の初期化
配列の初期化は次のように行うことができる。
code:array1.c
int main() {
int a5 = {0, 1, 2, 3, 4}; return 0;
}
初期値が指定してある場合は要素数を省略できる。
code:array2.c
int main() {
int a[] = {0, 1, 2, 3, 4};
return 0;
}
配列の要素へのアクセス
配列の個々の要素へのアクセスには添字演算子[]を使う。
code:access
ai //配列aの先頭からi番目の要素へアクセスする。 配列へのアクセスの添字は0から始まる。
code:access.c
int main() {
int a[] = {1, 2, 3, 4, 5};
return 0;
}
例えば上のコードで表示される値は2であり、1を表示するには添字を0にする必要がある。
ループを用いた配列の利用
for文を使うことで効率よく初期化したり要素を表示したりすることができる。
code:array3.c
int main() {
int i;
for(i = 0; i < 50; i++){
}
for(i = 0; i < 50; i++){
printf("a%d = %d\n" , i, ai); }
return 0;
}
配列への代入
C言語で配列の値を全て代入するにはfor文などで配列の全要素をそれぞれ代入する必要があります。
code:array3_for.c
int main() {
int a5 = {0, 1, 2, 3, 4}; for (int i=0; i<5; i++){
}
for(int i = 0; i < 5; i++){
printf("b%d = %d\n" , i, bi); }
return 0;
}
2次元配列
2次元配列とは配列自体が要素となっている配列のことである。
2次元配列も1次元配列と同じように初期化できる。
code:array4.c
int main() {
int a35 = {{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}, {10, 11, 12, 13, 14}}; return 0;
}
要素のアクセスについても1次元配列と同じように考えることができ、a[i]はi番目の配列を表しているためa[i][j]でa[i]のj番目の要素にアクセスすることができる。
code:array5.c
int main() {
for(int i=0; i<4; i++) {
for(int j=0; j<4; j++) {
}
}
for(int i=0; i<4; i++) {
for(int j=0; j<4; j++) {
}
printf("\n");
}
return 0;
}
文字配列
実は、今までプログラム中に書いてきた文字列リテラル(""記号で囲んだもの、"hello, world!\n"など)は文字の配列であった。したがって、Hello, World!を行うプログラムは次のようにも書ける。
code:helloworld.c
int main() {
char str14 = {'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '\n'}; printf("%s", str); //文字配列のprintfフォーマット指定子は%sを用いる
}
ここで、hello, world\nは13文字であるのになぜstr変数に14文字分の領域をとっているのだろうと疑問に思ってほしい。C言語では、文字配列は文字列の終端を示すために末尾にNULL文字'\0'を入れておかなければならないという決まりがある。NULL文字が入っていなかった場合、C言語では未定義動作となり、重篤な脆弱性になりうるため、必ず注意する必要がある。 文字列の要素アクセス
文字列リテラルは文字配列であるため、当然要素の文字へのアクセスができる。
code:charAccess.c
int main() {
char s[] = "Hello, World!";
for(int i=0; i<strlen(s); i+=2) {
}
return 0;
}
ここでは初めてstdio.h以外のものをincludeしている。string.hは文字列操作に用いる便利な関数が数多く定義されており、これをincludeすることでプログラム内で利用することができるようになる。ここでは文字列の長さを数えてくれる関数strlenを利用するためにincludeしている。
配列も標準入力からscanfで元の型と同じように入力値を代入できる。
入出力とループ・配列を使って、かなり実用的なプログラムが記述できるようになった。
次のプログラムは要素数と数値を複数受け取り、その平均値を求めてそれを出力するプログラムである。
code:average.c
int main() {
int n;
scanf("%d\n", &n);
for(int i=0; i<n; i++) scanf("%d", &ai); int sum = 0;
for(int i=0; i<n; i++) sum += ai; double avg = sum / (double)n;
printf("%lf\n", avg);
return 0;
}
まとめ
配列を用いることで関連する値を管理しやすくなる。
配列の添字は0から始まる。
配列はfor文を用いると効率よくアクセスできる。
文字配列の末尾にはNULL文字'\0'が必要。
練習問題
入力した文字列を逆順に出力するプログラムを書いてください。
入力した文字列の中の'a'だけを'*'に変換して出力するプログラムを書いてください。
挑戦問題
バブルソートとよばれるソートアルゴリズムがあります。これをC言語で実装し、与えられた数値を昇順に並べて出力するプログラムを書いてください。
code:入出力例
./a.out
5
1 4 8 2 7
result: 1 2 4 7 8
1000000以下の素数を全て求め、入力した数値番目のものを出力するプログラムを書いてください。
78470番目の素数は1000000を超えるため、入力数は78469以下であるとする。
エラトステネスの篩というアルゴリズムが利用できます。